﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;

using Common.Logging;

using NMemcached.Client.Configuration;

namespace NMemcached.Client
{
    /// <summary>
    /// A Memcached connection that communicates with multiple memcached servers
    /// </summary>
    /// <remarks>
    /// <para>The methods in this class do not make any assumptions about the server connection implementations. Each method inherited from the MemcachedConnection class are implemented in a way that calls the corresponding method in the underlying server connections.</para>
    /// </remarks>
    public class MemcachedClient : MemcachedConnection, IDisposable
    {
        private static readonly string CommandPattern;

        private readonly ILog _log = LogManager.GetCurrentClassLogger();

        private object _syncRoot;
        private bool _disposed;

        /// <summary>
        /// Occurs when an error occurs with a server connection
        /// </summary>
        public event EventHandler<ConnectionEventArgs> ConnectionError;

        /// <summary>
        /// Gets the IConnectionProvider used by the client to get server connections
        /// </summary>
        public IConnectionProvider ConnectionProvider { get; private set; }

        /// <summary>
        /// Gets or sets a value that determines if empty enties are returned for multi-get operations.
        /// </summary>
        public bool IncludeCacheMisses { get; set; }

        static MemcachedClient()
        {
            MemcachedClient.CommandPattern = StringTable.Get("log_CommandPattern");
        }

        /// <summary>
        /// Initializes a new instance of the MemcachedClient class
        /// </summary>
        /// <param name="servers">A collection of server names without port number</param>
        public MemcachedClient(params string[] servers)
            : this((IEnumerable<string>)servers)
        {
        }

        /// <summary>
        /// Initializes a new instance of the MemcachedClient class
        /// </summary>
        /// <param name="servers">A collection of server names without port number</param>
        public MemcachedClient(IEnumerable<string> servers)
            : this(new ConnectionProvider(new ServerConnectionCollection(servers)))
        {
        }

        /// <summary>
        /// Initializes a new instance of the MemcachedClient class
        /// </summary>
        /// <param name="connectionProvider">The IConnectionProvider used by the client to get server connections</param>
        public MemcachedClient(IConnectionProvider connectionProvider)
        {
            this.ConnectionProvider = connectionProvider;
            this._syncRoot = new object();
            this.IncludeCacheMisses = false;
            this._disposed = false;
        }

        /// <summary>
        /// Adds a key/value pair to a Memcached server if the data does not already exist.
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Add(string key, object data, DateTime expiration)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Add, key, data, expiration);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Adds data to an existing cache entry after the existing data
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Append(string key, object data)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Append, key, data, DateTime.MinValue);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Stores data for an existing key only if it has not been updated since the last get
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <param name="casUnique">The unique value of an an existing entry</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode CheckAndSet(string key, object data, DateTime expiration, ulong casUnique)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Set, key, data, expiration, casUnique);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Closes all connections to Memcached servers
        /// </summary>
        public override void Close()
        {
            lock (this._syncRoot)
            {
                foreach (ServerConnection server in this.ConnectionProvider.Servers)
                {
                    try
                    {
                        server.Close();
                    }
                    catch (SocketException se)
                    {
                        this.OnConnectionError(server, se);
                    }
                }
            }
        }

        /// <summary>
        /// Creates a new MemcachedClient from the application configuration
        /// </summary>
        /// <returns>A new MemcachedClient defined by the application configuration</returns>
        /// <remarks>
        /// <para>A MemcachedClient instance can be created using settings read from the applicaiton or web configuration file. The built-in configuration uses the included connection provider and does not have the ability to use custom connection providers.</para>
        /// <para>Example:</para>
        /// <code>
        /// &lt;configSections&gt;
        ///     &lt;section name="nmemcached.client" type="NMemcached.Client.Configuration.NMemcachedConfigurationSection, NMemcached.Client" /&gt;
        /// &lt;/configSections&gt;
        /// &lt;nmemcached.client xmlns="http://memcached.protectedvoid.com/2011/06/configuration"&gt;
        ///     &lt;connectionProvider&gt;
        ///         &lt;add server="localhost" compressionAlgorithm="Deflate" /&gt;
        ///         &lt;add server="someRemoteServer" port="15455" compressionAlgorithm="GZip" /&gt;
        ///     &lt;/connectionProvider&gt;
        /// &lt;/nmemcached.client&gt;
        /// </code>
        /// <para>Note: An XSD is in the source tree and included with the binary distributions for intellisense.</para>
        /// <para>The ConnectionProvider element has an optional "hashAlgorithm" attribute that specifies the algorithm to use for they consistent hashing algorithm. The possible values are:</para>
        /// <list type="bullet">
        ///     <item>
        ///         <term>fnv32</term>
        ///         <description>The FNV algorithm that produces a 32 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>fnv1a32 (default)</term>
        ///         <description>The FNV1a algorithm that produces a 32 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>fnv64</term>
        ///         <description>The FNV algorithm that produces a 64 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>fnv1a64</term>
        ///         <description>The FNV1a algorithm that produces a 64 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>md5</term>
        ///         <description>The MD5 algorithm that produces a 128 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>sha1</term>
        ///         <description>The SHA1 algorithm that produces a 160 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>sha256</term>
        ///         <description>The SHA1 algorithm that produces a 256 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>sha384</term>
        ///         <description>The SHA1 algorithm that produces a 384 bit hash value</description>
        ///     </item>
        ///     <item>
        ///         <term>sha512</term>
        ///         <description>The SHA1 algorithm that produces a 512 bit hash value</description>
        ///     </item>
        /// </list>
        /// <para>Each "add" child element of "connectionProvider" represents a single Memcached server. The only required attribute is the "server" attribute that must contian the host name ir IP address of the destination. Any combination of optional attributes can be provided to configure individual server connections:</para>
        /// <list type="bullet">
        ///     <item>
        ///         <term>port</term>
        ///         <description>A valid TCP/UDP port number. (default: "11211")</description>
        ///     </item>
        ///     <item>
        ///         <term>protocol</term>
        ///         <description>The Memcached communication protocol. Valid values are "text" and "binary". (default: "text")</description>
        ///     </item>
        ///     <item>
        ///         <term>transport</term>
        ///         <description>The transport protocol for the connection. Valid values are "tcp", "udp", "connectionOriented", "connectionless". (default: "tcp")</description>
        ///     </item>
        ///     <item>
        ///         <term>useCompression</term>
        ///         <description>Determines if data compression is used for items sent to the cache. (default: "false")</description>
        ///     </item>
        ///     <item>
        ///         <term>compressionAlgorithm</term>
        ///         <description>The algorithm to use for data compression if "useCompression" is set to "true". Valid values are "GZip" and "Deflate". (default: "GZip")</description>
        ///     </item>
        ///     <item>
        ///         <term>paylodEncoding</term>
        ///         <description>The character encoding of the data sent to the cache. Value values are "ASCII", "BigEndianUnicode", "Unicode", "UTF32", "UTF7", and "UTF8". (default: "UTF8")</description>
        ///     </item>
        ///     <item>
        ///         <term>protocolEncoding</term>
        ///         <description>The character encoding of the memcached protocol. Valid values are the same as the payloadEncoding attribute. (default: "UTF8")</description>
        ///     </item>
        ///     <item>
        ///         <term>sendTimeout</term>
        ///         <description>The number of milliseconds to wait to send data before throwing an exception. (default: 5000)</description>
        ///     </item>
        ///     <item>
        ///         <term>receiveTimeout</term>
        ///         <description>The number of milliseconds to wait to receive data before throwing an exception. (default: 5000)</description>
        ///     </item>
        /// </list>
        /// </remarks>
        public static MemcachedClient Create()
        {
            return NMemcachedConfiguration.CreateClient();
        }

        /// <summary>
        /// Decreases a numeric cached value at the specified key by the specified amount
        /// </summary>
        /// <param name="key">The cache entry to increment</param>
        /// <param name="count">The amount by which the value is decremented</param>
        /// <returns>The new value of the data after the decrement operation was carried out</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ulong Decrement(string key, ulong count)
        {
            ulong result = 0;
            ServerConnection connection = this.ConnectionProvider.GetConnection(key);
            //No connection found for the key and/or no servers available
            if (connection != null)
            {
                try
                {
                    result = connection.Decrement(key, count);
                }
                catch (ArgumentNullException e)
                {
                    throw e;
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(connection, se);
                }
            }

            this._log.Debug(m => m(MemcachedClient.CommandPattern, "Decrement", key, result));

            return result;
        }

        /// <summary>
        /// Removes a cache entry
        /// </summary>
        /// <param name="key">The key of the entry to be deleted</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Delete(string key)
        {
            ResponseCode result = ResponseCode.KeyNotFound;
            ServerConnection connection = this.ConnectionProvider.GetConnection(key);
            //Don't bother trying to delete if none of the
            //servers have the key
            if (connection != null)
            {
                try
                {
                    result = connection.Delete(key);
                }
                catch (ArgumentNullException e)
                {
                    throw e;
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(connection, se);
                }
            }

            this._log.Debug(m => m(MemcachedClient.CommandPattern, "Delete", key, result));

            return result;
        }

        /// <summary>
        /// Immediately invalidates all cached items in all servers
        /// </summary>
        /// <remarks>
        /// <para>The "flush_all" command has no response other than OK unless there is an error</para>
        /// </remarks>
        public override void FlushAll()
        {
            this._log.Warn("Flush all called");

            lock (this._syncRoot)
            {
                foreach (ServerConnection server in this.ConnectionProvider.Servers)
                {
                    try
                    {
                        server.FlushAll();
                    }
                    catch (SocketException se)
                    {
                        this.OnConnectionError(server, se);
                    }
                }
            }
        }

        /// <summary>
        /// Invalidates all cached items in all servers after the specified delay
        /// </summary>
        /// <param name="delay">The amount of time to pause prior to executing the "flush_all" command.</param>
        /// <remarks>
        /// <para>The "flush_all" command has no response other than OK unless there is an error.</para>
        /// </remarks>
        public override void FlushAll(TimeSpan delay)
        {
            this._log.Warn(StringTable.Get("log_Flush"));

            lock (this._syncRoot)
            {
                foreach (ServerConnection server in this.ConnectionProvider.Servers)
                {
                    try
                    {
                        server.FlushAll(delay);
                    }
                    catch (SocketException se)
                    {
                        this.OnConnectionError(server, se);
                    }
                }
            }
        }

        /// <summary>
        /// Reads a single entry from the cache
        /// </summary>
        /// <param name="key">The key of the item to get from the cache</param>
        /// <returns>The data from the cache associated with the specified key</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override object Get(string key)
        {
            object found = null;
            ServerConnection connection = this.ConnectionProvider.GetConnection(key);
            if (connection != null)
            {
                try
                {
                    found = connection.Get(key);
                }
                catch (ArgumentNullException e)
                {
                    throw e;
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(connection, se);
                }
            }
            return found;
        }

        /// <summary>
        /// Reads multiple entries from the cache
        /// </summary>
        /// <param name="keys">A list of keys for which the data is read</param>
        /// <returns>A keyed collection of items read from the cache</returns>
        /// <exception cref="System.ArgumentNullException">A null key was encountered</exception>
        public override IDictionary<string, object> Get(params string[] keys)
        {
            IDictionary<ServerConnection, List<string>> serverKeyMap = this.ConnectionProvider.GetConnections(keys);

            Dictionary<string, object> allItems = null;
            IDictionary<string, object> result = null;

            if (serverKeyMap != null)
            {
                allItems = new Dictionary<string, object>();
                foreach (ServerConnection server in serverKeyMap.Keys)
                {
                    try
                    {
                        result = server.Get(serverKeyMap[server].ToArray());
                        if (result != null)
                        {
                            foreach (KeyValuePair<string, object> pair in result)
                            {
                                if (pair.Value == null)
                                {
                                    //Only include cache misses if requested
                                    if (this.IncludeCacheMisses)
                                    {
                                        allItems.Add(pair.Key, null);
                                    }
                                }
                                else
                                {
                                    allItems.Add(pair.Key, pair.Value);
                                }
                            }
                        }
                    }
                    catch (ArgumentNullException e)
                    {
                        throw e;
                    }
                    catch (SocketException se)
                    {
                        this.OnConnectionError(server, se);
                        //Add the items that don't already exist if cache misses are requested
                        if (this.IncludeCacheMisses)
                        {
                            foreach (string serverKey in serverKeyMap[server])
                            {
                                if (!allItems.ContainsKey(serverKey))
                                {
                                    allItems.Add(serverKey, null);
                                }
                            }
                        }
                    }
                }
            }

            return allItems;
        }

        /// <summary>
        /// Reads a single entry from the cache and returns the data with detail information
        /// </summary>
        /// <param name="key">The key of the item to get from the cache</param>
        /// <returns>The data from the cache associated with the specified key with detail information</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override CacheDetail GetWithDetail(string key)
        {
            CacheDetail found = null;
            ServerConnection connection = this.ConnectionProvider.GetConnection(key);
            if (connection != null)
            {
                try
                {
                    found = connection.GetWithDetail(key);
                }
                catch (ArgumentNullException e)
                {
                    throw e;
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(connection, se);
                }
            }
            return found;
        }

        /// <summary>
        /// Reads multiple entries from the cache with detail information
        /// </summary>
        /// <param name="keys">A list of keys for which the data is read</param>
        /// <returns>A keyed collection of items read from the cache with detail information</returns>
        /// <exception cref="System.ArgumentNullException">A null key was encountered</exception>
        public override IDictionary<string, CacheDetail> GetWithDetail(params string[] keys)
        {
            IDictionary<ServerConnection, List<string>> serverKeyMap = this.ConnectionProvider.GetConnections(keys);

            Dictionary<string, CacheDetail> allItems = null;
            IDictionary<string, CacheDetail> result = null;

            if (serverKeyMap != null)
            {
                allItems = new Dictionary<string, CacheDetail>();
                foreach (ServerConnection server in serverKeyMap.Keys)
                {
                    try
                    {
                        result = server.GetWithDetail(serverKeyMap[server].ToArray());
                        if (result != null)
                        {
                            foreach (KeyValuePair<string, CacheDetail> pair in result)
                            {
                                if (pair.Value == null)
                                {
                                    //Only include cache misses if requested
                                    if (this.IncludeCacheMisses)
                                    {
                                        allItems.Add(pair.Key, null);
                                    }
                                }
                                else
                                {
                                    allItems.Add(pair.Key, pair.Value);
                                }
                            }
                        }
                    }
                    catch (ArgumentNullException e)
                    {
                        throw e;
                    }
                    catch (SocketException se)
                    {
                        this.OnConnectionError(server, se);
                        //Add the items that don't already exist if cache misses are requested
                        if (this.IncludeCacheMisses)
                        {
                            foreach (string serverKey in serverKeyMap[server])
                            {
                                if (!allItems.ContainsKey(serverKey))
                                {
                                    allItems.Add(serverKey, null);
                                }
                            }
                        }
                    }
                }
            }

            return allItems;
        }

        /// <summary>
        /// Increases a numeric cached value at the specified key by the specified amount
        /// </summary>
        /// <param name="key">The cache entry to increment</param>
        /// <param name="count">The amount by which the value is incremented</param>
        /// <returns>The new value of the data after the increment operation was carried out</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ulong Increment(string key, ulong count)
        {
            ulong result = 0;
            ServerConnection connection = this.ConnectionProvider.GetConnection(key);
            //No connection found for the key and/or no servers available
            if (connection != null)
            {
                try
                {
                    result = connection.Increment(key, count);
                }
                catch (ArgumentNullException e)
                {
                    throw e;
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(connection, se);
                }
            }

            this._log.Debug(m => m(MemcachedClient.CommandPattern, "Increment", key, result));

            return result;
        }

        /// <summary>
        /// Adds data to an existing cache entry before the existing data
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Prepend(string key, object data)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Prepend, key, data, DateTime.MinValue);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Replaces the data associated with an existing cache entry
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Replace(string key, object data, DateTime expiration)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Replace, key, data, expiration);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Adds data to the cache, overwriting any data in an existing entry
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Set(string key, object data, DateTime expiration)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Set, key, data, expiration);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Gets the statistics for each server
        /// </summary>
        /// <returns>A dictionary keyed by connection that contains the statistics for each server</returns>
        public IDictionary<ServerConnection, IDictionary<string, string>> Stats()
        {
            Dictionary<ServerConnection, IDictionary<string, string>> stats = new Dictionary<ServerConnection, IDictionary<string, string>>();
            foreach (ServerConnection server in this.ConnectionProvider.Servers)
            {
                try
                {
                    stats.Add(server, server.Stats());
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(server, se);
                }
            }
            if (stats.Count == 0)
            {
                stats = null;
            }
            return stats;
        }

        /// <summary>
        /// Gets the versions of each server
        /// </summary>
        /// <returns>A collection of server version numbers.</returns>
        public IEnumerable<string> Versions()
        {
            List<string> versions = new List<string>();
            foreach (ServerConnection server in this.ConnectionProvider.Servers)
            {
                try
                {
                    versions.Add(server.Version());
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(server, se);
                }
            }
            if (versions.Count == 0)
            {
                versions = null;
            }
            return versions;
        }

        /// <summary>
        /// Releases the unmanaged resources used by the MemcachedClient
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        //Protected methods

        /// <summary>
        /// Releases the unmanaged resources used by the MemcachedClient, and optionally disposes of the managed resources
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to releases only unmanaged resources</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                {
                }

                if (this.ConnectionProvider != null)
                {
                    this.ConnectionProvider.Dispose();
                }

                this._disposed = true;
            }
        }

        //Private methods

        private void OnConnectionError(ServerConnection connection, SocketException se)
        {
            if (connection == null)
            {
                throw new ArgumentNullException("connection");
            }

            if (this.ConnectionError != null)
            {
                //Ensure that only one thread can call this at a time
                //by locking the instance rather than this._syncRoot
                lock (this)
                {
                    this.ConnectionProvider.DisableConnection(connection);
                }
                this.ConnectionError(this, new ConnectionEventArgs(connection, se));
            }
        }

        private ResponseCode SendStorageCommand(OpCode operation, string key, object data, DateTime expiration)
        {
            return this.SendStorageCommand(operation, key, data, expiration, null);
        }

        private ResponseCode SendStorageCommand(OpCode operation, string key, object data, DateTime expiration, ulong? casUnique)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            ResponseCode result = ResponseCode.ItemNotStored;
            ServerConnection connection = this.ConnectionProvider.GetConnection(key);
            //Null here means no available servers
            if (connection != null)
            {
                try
                {
                    if (casUnique == null)
                    {
                        switch (operation)
                        {
                            case OpCode.Add:
                                result = connection.Add(key, data, expiration);
                                break;
                            case OpCode.Append:
                                result = connection.Append(key, data);
                                break;
                            case OpCode.Prepend:
                                result = connection.Prepend(key, data);
                                break;
                            case OpCode.Replace:
                                result = connection.Replace(key, data, expiration);
                                break;
                            case OpCode.Set:
                                result = connection.Set(key, data, expiration);
                                break;
                        }
                    }
                    else
                    {
                        result = connection.CheckAndSet(key, data, expiration, (ulong)casUnique);
                    }
                }
                catch (SocketException se)
                {
                    this.OnConnectionError(connection, se);
                }
            }

            this._log.Debug(m => m(MemcachedClient.CommandPattern, operation, key, result));

            return result;
        }
    }
}